/*
 * Copyright 2002-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.aop.aspectj.annotation;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.aspectj.lang.reflect.PerClauseKind;

import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJProxyUtils;
import org.springframework.aop.aspectj.SimpleAspectInstanceFactory;
import org.springframework.aop.framework.ProxyCreatorSupport;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

/**
 * AspectJ-based proxy factory, allowing for programmatic building
 * of proxies which include AspectJ aspects (code style as well
 * Java 5 annotation style).
 *
 * @author Rob Harrop
 * @author Juergen Hoeller
 * @author Ramnivas Laddad
 * @see #addAspect(Object)
 * @see #addAspect(Class)
 * @see #getProxy()
 * @see #getProxy(ClassLoader)
 * @see org.springframework.aop.framework.ProxyFactory
 * @since 2.0
 */
@SuppressWarnings("serial")
public class AspectJProxyFactory extends ProxyCreatorSupport {

    /**
     * Cache for singleton aspect instances
     */
    private static final Map<Class<?>, Object> aspectCache = new ConcurrentHashMap<>();

    private final AspectJAdvisorFactory aspectFactory = new ReflectiveAspectJAdvisorFactory();


    /**
     * Create a new AspectJProxyFactory.
     */
    public AspectJProxyFactory() {
    }

    /**
     * Create a new AspectJProxyFactory.
     * <p>Will proxy all interfaces that the given target implements.
     *
     * @param target the target object to be proxied
     */
    public AspectJProxyFactory(Object target) {
        Assert.notNull(target, "Target object must not be null");
        setInterfaces(ClassUtils.getAllInterfaces(target));
        setTarget(target);
    }

    /**
     * Create a new {@code AspectJProxyFactory}.
     * No target, only interfaces. Must add interceptors.
     */
    public AspectJProxyFactory(Class<?>... interfaces) {
        setInterfaces(interfaces);
    }


    /**
     * Add the supplied aspect instance to the chain. The type of the aspect instance
     * supplied must be a singleton aspect. True singleton lifecycle is not honoured when
     * using this method - the caller is responsible for managing the lifecycle of any
     * aspects added in this way.
     *
     * @param aspectInstance the AspectJ aspect instance
     */
    public void addAspect(Object aspectInstance) {
        Class<?> aspectClass = aspectInstance.getClass();
        String aspectName = aspectClass.getName();
        AspectMetadata am = createAspectMetadata(aspectClass, aspectName);
        if (am.getAjType().getPerClause().getKind() != PerClauseKind.SINGLETON) {
            throw new IllegalArgumentException(
                    "Aspect class [" + aspectClass.getName() + "] does not define a singleton aspect");
        }
        addAdvisorsFromAspectInstanceFactory(
                new SingletonMetadataAwareAspectInstanceFactory(aspectInstance, aspectName));
    }

    /**
     * Add an aspect of the supplied type to the end of the advice chain.
     *
     * @param aspectClass the AspectJ aspect class
     */
    public void addAspect(Class<?> aspectClass) {
        String aspectName = aspectClass.getName();
        AspectMetadata am = createAspectMetadata(aspectClass, aspectName);
        MetadataAwareAspectInstanceFactory instanceFactory = createAspectInstanceFactory(am, aspectClass, aspectName);
        addAdvisorsFromAspectInstanceFactory(instanceFactory);
    }


    /**
     * Add all {@link Advisor Advisors} from the supplied {@link MetadataAwareAspectInstanceFactory}
     * to the current chain. Exposes any special purpose {@link Advisor Advisors} if needed.
     *
     * @see AspectJProxyUtils#makeAdvisorChainAspectJCapableIfNecessary(List)
     */
    private void addAdvisorsFromAspectInstanceFactory(MetadataAwareAspectInstanceFactory instanceFactory) {
        List<Advisor> advisors = this.aspectFactory.getAdvisors(instanceFactory);
        Class<?> targetClass = getTargetClass();
        Assert.state(targetClass != null, "Unresolvable target class");
        advisors = AopUtils.findAdvisorsThatCanApply(advisors, targetClass);
        AspectJProxyUtils.makeAdvisorChainAspectJCapableIfNecessary(advisors);
        AnnotationAwareOrderComparator.sort(advisors);
        addAdvisors(advisors);
    }

    /**
     * Create an {@link AspectMetadata} instance for the supplied aspect type.
     */
    private AspectMetadata createAspectMetadata(Class<?> aspectClass, String aspectName) {
        AspectMetadata am = new AspectMetadata(aspectClass, aspectName);
        if (!am.getAjType().isAspect()) {
            throw new IllegalArgumentException("Class [" + aspectClass.getName() + "] is not a valid aspect type");
        }
        return am;
    }

    /**
     * Create a {@link MetadataAwareAspectInstanceFactory} for the supplied aspect type. If the aspect type
     * has no per clause, then a {@link SingletonMetadataAwareAspectInstanceFactory} is returned, otherwise
     * a {@link PrototypeAspectInstanceFactory} is returned.
     */
    private MetadataAwareAspectInstanceFactory createAspectInstanceFactory(
            AspectMetadata am, Class<?> aspectClass, String aspectName) {

        MetadataAwareAspectInstanceFactory instanceFactory;
        if (am.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
            // Create a shared aspect instance.
            Object instance = getSingletonAspectInstance(aspectClass);
            instanceFactory = new SingletonMetadataAwareAspectInstanceFactory(instance, aspectName);
        } else {
            // Create a factory for independent aspect instances.
            instanceFactory = new SimpleMetadataAwareAspectInstanceFactory(aspectClass, aspectName);
        }
        return instanceFactory;
    }

    /**
     * Get the singleton aspect instance for the supplied aspect type. An instance
     * is created if one cannot be found in the instance cache.
     */
    private Object getSingletonAspectInstance(Class<?> aspectClass) {
        // Quick check without a lock...
        Object instance = aspectCache.get(aspectClass);
        if (instance == null) {
            synchronized (aspectCache) {
                // To be safe, check within full lock now...
                instance = aspectCache.get(aspectClass);
                if (instance != null) {
                    return instance;
                }
                instance = new SimpleAspectInstanceFactory(aspectClass).getAspectInstance();
                aspectCache.put(aspectClass, instance);
            }
        }
        return instance;
    }


    /**
     * Create a new proxy according to the settings in this factory.
     * <p>Can be called repeatedly. Effect will vary if we've added
     * or removed interfaces. Can add and remove interceptors.
     * <p>Uses a default class loader: Usually, the thread context class loader
     * (if necessary for proxy creation).
     *
     * @return the new proxy
     */
    @SuppressWarnings("unchecked")
    public <T> T getProxy() {
        return (T) createAopProxy().getProxy();
    }

    /**
     * Create a new proxy according to the settings in this factory.
     * <p>Can be called repeatedly. Effect will vary if we've added
     * or removed interfaces. Can add and remove interceptors.
     * <p>Uses the given class loader (if necessary for proxy creation).
     *
     * @param classLoader the class loader to create the proxy with
     * @return the new proxy
     */
    @SuppressWarnings("unchecked")
    public <T> T getProxy(ClassLoader classLoader) {
        return (T) createAopProxy().getProxy(classLoader);
    }

}
